/**
 * © 2009 Susanta Tewari
 * This file is part of genomemap.
 *
 * genomemap is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * genomemap is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with genomemap.  If not, see <http://www.gnu.org/licenses/>.
 */
package genomemap.data;

import genomemap.io.DataIO;
import genomemap.util.AbstractJob;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * Loads data from filesystem for various parts of the application.
 * TODO USE MEMORY MANAGER FOR IMPROVED FLEXIBILITY ON USING MULTIPLE CHROMOSOMES IN COMPUTATION
 *
 *
 * @version 1.0 11/26/2009
 * @author Susanta Tewari
 */
public class Chromosome extends AbstractJob {

    // <editor-fold defaultstate="collapsed" desc="Static Data">
    /**
     * An instance of Chromosome for each chromosome
     */
    private static Chromosome[] instances = new Chromosome[7];

    /**
     * chromosome length in bases
     */
    private final int[] CH_LENGTHS = new int[]{10300, 4600, 5100, 5700, 9200, 4000, 4000};
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Instance Data">

    /**
     * linkage group corresponding to this chromosome
     */
    private int linkageGroup;

    /**
     * Genes for this chromosome in the order present in the dataset. This order acts as the
     * reference for translating gene orders to orders of natural numbers and vice versa.
     * 
     */
    private final List<String> genes;

    /**
     * Probes for this chromosome in the order present in the dataset. This order acts as the
     * reference for translating probe orders to orders of natural numbers and vice versa.
     */
    private final List<String> probes;

    /**
     * Clones for this chromosome in the order present in the dataset. This order acts as the
     * reference for translating clone orders to orders of natural numbers and vice versa.
     */
    private final List<String> clones;

    /**
     * Genotypes of the offsprings for this chromosome. Each column represents the full
     * genotype for an offspring. This primitive form of data is needed to create genotype data
     * for various gene orders. Notice that the data is order dependent. This variable is
     * initialized lazily.
     */
    private byte[][] genotypeData;

    /**
     * clone-probe hybridization data for this chromosome. Rows indicate clones and
     * columns indicate probes. This primitive form of data is needed to create genotype data
     * for various gene orders. Notice that the data is order dependent. This variable is
     * initialized lazily.
     */
    private byte[][] hybridizationData;
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Constructors">
    /**
     * Initializes the instance data for the dataloader.
     * @param linkageGroup
     */
    private Chromosome(int linkageGroup) {

        this.linkageGroup = linkageGroup;

        setMessage("loading gene names");
        genes = Collections.unmodifiableList(Loader.loadLines(DataIO.getInstance().
                gene_names_stream(linkageGroup)));

        setMessage("loading probe names");
        probes = Collections.unmodifiableList(Loader.loadLines(DataIO.getInstance().
                probe_names_file(linkageGroup)));

        setMessage("loading clone names");
        clones = Collections.unmodifiableList(Loader.loadLines(DataIO.getInstance().
                clone_names_file(linkageGroup)));

        setMessage("loading genotype data");
        genotypeData = Loader.load2DByteArray(DataIO.getInstance().genotype_data_file(
                linkageGroup));

        setMessage("loading hybridization data");
        hybridizationData = Loader.load2DByteArray(DataIO.getInstance().
                hybridization_data_file(linkageGroup));


    }

    /**
     * Returns an instance of Chromosome for the given chromosome
     * @param linkageGroup chromosome for the dataloader
     * @return an instance of Chromosome for the given chromosome
     */
    public static Chromosome getInstance(Integer linkageGroup) {

        if (instances[linkageGroup - 1] == null) {

            Chromosome loader = new Chromosome(linkageGroup);
            instances[linkageGroup - 1] = loader;

        }

        return instances[linkageGroup - 1];

    }

    public static void clearCache() {

        for (int i = 0; i < instances.length; i++) instances[i] = null;
    }

    public static void removeInstance(Integer linkageGroup) {

        instances[linkageGroup - 1] = null;
    }
    // </editor-fold>

    /**
     *length of the chromosome in bases
     * @return
     */
    public Integer getLength() {
        return CH_LENGTHS[linkageGroup - 1];
    }

    /**
     * get gene names
     * @return
     */
    public Set<String> getGenes() {

        return new HashSet<String>(genes);
    }

    // <editor-fold defaultstate="collapsed" desc="Potentially To Be Removed">
    private List<String> getGenes(List<Integer> geneOrder) {
        return getNamesFromOrder(geneOrder, genes);
    }

    /**
     *
     * @param names
     * @return gene order, 1-based ( not index based)
     */
    private List<Integer> getGeneOrder(List<String> names) {
        return getOrderFromNames(names, genes);
    }

    private Integer getGeneOrder(String name) {
        return genes.indexOf(name) + 1;
    }

    private String getGene(Integer geneOrder) {
        return genes.get(geneOrder - 1);
    }

    /**
     * probe names corresponding to the order
     * @param probeOrder
     * @return
     */
    private List<String> getProbes(List<Integer> probeOrder) {
        return getNamesFromOrder(probeOrder, probes);
    }

    /**
     *
     * @param name probe name
     * @return 1 based order
     */
    private Integer getProbeOrder(String name) {
        return probes.indexOf(name) + 1;
    }

    private String getProbe(int probeOrder) {
        return probes.get(probeOrder - 1);
    }

    private String getClone(Integer cloneOrder) {
        return clones.get(cloneOrder);
    }

    /**
     *
     * @param name clone name
     * @return 1 based order
     */
    private Integer getCloneOrder(String name) {
        int cloneIndex = clones.indexOf(name);
        if (cloneIndex < 0) {
            throw new RuntimeException("clone : " + name + " not found");
        }
        return cloneIndex + 1;
    }

    // </editor-fold>
    /**
     * Provides genes that have been tagged
     * @return the set of assigned genes
     */
    public Set<String> getGenesTagged() {
        return AssignmentManager.getInstance(linkageGroup).g_map.keySet();
    }

    /**
     * Provides genes that have been tagged to this <code>clone</code>
     * @param clone the clone for which the tagged genes are sought
     * @return the set of assigned genes to this <code>clone</code> or an empty set
     */
    public Set<String> getGenesTagged(String clone) {

        Set<String> result = new HashSet<String>();

        Set<String> tagged_genes = AssignmentManager.getInstance(linkageGroup).c_map.get(clone);

        if (tagged_genes == null) return result;

        result.addAll(tagged_genes);

        return result;
    }

    /**
     * Provides the probe names in the order present in the dataset. This method also
     * applies performance enhancing techniques like object caching etc.
     * @return array of probe names
     */
    public Set<String> getProbes() {

        return new HashSet<String>(probes);
    }

    /**
     *
     * @param probeNames probe names
     * @return numerical order of the probes
     * @throws RuntimeException if a probe name is not found
     */
    public List<Integer> getProbeOrder(List<String> probeNames) {

        return getOrderFromNames(probeNames, probes);
    }

    /**
     * Provides probes that have been tagged
     * @return the set of assigned probes or an empty set otherwise
     */
    public Set<String> getProbesTagged() {
        Set<String> result = new HashSet<String>(AssignmentManager.getInstance(linkageGroup).c_map.
                keySet());
        result.retainAll(probes);
        return result;
    }

    /**
     * Provides probes that have been tagged to this <code>gene</code>
     * @param gene the gene for which the tagged probes are sought
     * @return the set of probes tagged to this <code>gene</code> or an empty set otherwise
     */
    public Set<String> getProbesTagged(String gene) {

        Set<String> result = new HashSet<String>();

        Set<String> tagged_clones = AssignmentManager.getInstance(linkageGroup).g_map.get(gene);
        if (tagged_clones == null) return result;

        result.addAll(tagged_clones);
        result.retainAll(probes);

        return result;
    }

    public boolean isProbe(String clone) {
        return probes.indexOf(clone) != -1;
    }

    /**
     * get clone names
     * @return
     */
    public Set<String> getClones() {

        return new HashSet<String>(clones);
    }

    public List<Integer> getCloneOrder(List<String> names) {
        return getOrderFromNames(names, clones);
    }

    public Set<String> getHybridizingProbes(String clone) {

        Set<String> result = new HashSet<String>();
        int cloneIndex = getCloneOrder(clone) - 1;
        for (int i = 0; i < hybridizationData[0].length; i++) {
            if (hybridizationData[cloneIndex][i] == 1) {
                result.add(getProbe(i + 1));
            }
        }

        return result;

    }

    /**
     * The resulting set does not contain the specified probes.
     * @param probe1
     * @param probe2
     * @return
     */
    public Set<String> getConnectingClones(String probe1, String probe2) {

        Set<String> result = new HashSet<String>();

        int probeIndex1 = probes.indexOf(probe1);
        int probeIndex2 = probes.indexOf(probe2);

        for (int i = 0; i < hybridizationData.length; i++) {
            if (hybridizationData[i][probeIndex1] == 1 && hybridizationData[i][probeIndex2] == 1) {
                result.add(clones.get(i));
            }
        }

        result.remove(probe1);
        result.remove(probe2);

        return result;

    }

    /**
     * The resulting set does not contain the specified probe.
     * @param probe
     * @return
     */
    public Set<String> getConnectingClones(String probe) {

        Set<String> result = new HashSet<String>();

        int probeIndex = probes.indexOf(probe);

        for (int i = 0; i < hybridizationData.length; i++) {
            if (hybridizationData[i][probeIndex] == 1) {
                result.add(clones.get(i));
            }
        }

        result.remove(probe);
        return result;

    }

    public boolean isHybridizing(String clone, String probe) {

        int cloneIndex = clones.indexOf(clone);
        int probeIndex = probes.indexOf(probe);
        if (hybridizationData[cloneIndex][probeIndex] == 1) return true;
        else return false;

    }

    /**
     * Provides clones that have been tagged
     * @return the set of assigned clones
     */
    Set<String> getClonesTagged() {
        return AssignmentManager.getInstance(linkageGroup).c_map.keySet();
    }

    /**
     * Provides clones that have been tagged to this <code>gene</code>. Note that the clones
     * here are not probes.
     * @param gene the gene for which the tagged clones are sought
     * @return the set of clones tagged to this <code>gene</code>
     */
    Set<String> getClonesTagged(String gene) {
        return AssignmentManager.getInstance(linkageGroup).g_map.get(gene);
    }

    /**
     * get genotype data
     * rows indicate offspring and columns genes
     * @param geneticMap
     * @return
     */
    public byte[][] getGenotypeData(List<String> geneticMap) {

        List<Integer> order = getGeneOrder(geneticMap);
        byte[][] data = new byte[genotypeData.length][order.size()];
        for (int i = 0; i < data.length; i++) {
            for (int j = 0; j < data[0].length; j++) {
                data[i][j] = genotypeData[i][order.get(j) - 1];
            }
        }

        return data;
    }

    /**
     * get genotype data
     * @param physicalMap
     * @return
     */
    public byte[][] getHybridizationData(List<String> physicalMap) {

        List<Integer> order = getProbeOrder(physicalMap);
        byte[][] data = new byte[hybridizationData.length][order.size()];
        for (int i = 0; i < data.length; i++) {
            for (int j = 0; j < data[0].length; j++) {
                data[i][j] = hybridizationData[i][order.get(j) - 1];
            }
        }

        return data;
    }

    /**
     * Probability of false positive
     * @return
     */
    public double getProbFalsePos() {
        return 0.00002;
    }

    /**
     * probability of false negative
     * @return
     */
    public double getProbFalseNeg() {
        return 0.00002;
    }

    private static class Loader {

        private static List<String> loadLines(InputStream is) {

            BufferedReader bufferedReader = null;

            String line = null;

            try {

                bufferedReader = new BufferedReader(new InputStreamReader(is));

                List<String> list = new ArrayList<String>();

                while ((line = bufferedReader.readLine()) != null) {

                    list.add(line);
                }

                return list;


            } catch (IOException ex) {

                ex.printStackTrace();

                return null;

            } finally {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

        }

        /**
         * Reads a character inputstream of comma separated values (.csv format ) into a 2D integer
         * array. The array need not be regular.
         * @param is the character inputstream to read from
         * @return a 2D integer array or <code>null</code> if any non-integer character is found
         * or any exception related to inputstream is thrown.
         */
        private static byte[][] load2DByteArray(InputStream is) {

            BufferedReader bufferedReader = null;
            String line = null;
            try {

                bufferedReader = new BufferedReader(new InputStreamReader(is));

                List<List<String>> dataList = new ArrayList<List<String>>();
                while ((line = bufferedReader.readLine()) != null) {
                    dataList.add(Arrays.asList(line.split(",")));
                }

                byte[][] data = new byte[dataList.size()][];
                for (int i = 0; i < dataList.size(); i++) {
                    String[] row = (String[]) dataList.get(i).toArray();
                    byte[] val = new byte[row.length];
                    for (int j = 0; j < val.length; j++) {
                        val[j] = Byte.parseByte(row[j]);
                    }
                    data[i] = val;
                }

                return data;

            } catch (IOException ex) {

                ex.printStackTrace();

                return null;

            } finally {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

        }

    }

    private List<String> getNamesFromOrder(List<Integer> order, List<String> names) {

        List<String> result = new ArrayList<String>(order.size());
        for (int position : order) {
            result.add(names.get(position - 1));

        }
        return result;
    }

    private List<Integer> getOrderFromNames(List<String> vals, List<String> names) throws RuntimeException {

        List<Integer> order = new ArrayList<Integer>(vals.size());

        int index = -1;

        for (String val : vals) {

            index = names.indexOf(val);

            if (index == -1) {
                throw new RuntimeException("Probe: " + val + "not found");
            }

            order.add(index + 1);

        }

        return order;
    }

}
